using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Tomahawk.Runtime.Resources;
using Tomahawk.Runtime.Rendering.Interfaces;
using Tomahawk.Resources.Particles;
using Tomahawk.Runtime.Rendering.Objects;
using Tomahawk.Runtime.Rendering.Materials;

namespace Tomahawk.Runtime.Rendering.Particles
{
    /// <summary>
    /// The main component in charge of displaying particles.
    /// </summary>
    public sealed class ParticleSystem : Entity, IRenderable
    {
        #region Fields

        // Particle settings that controls the appearance and animation of this particle system.
        private string assetName = "";

        // Custom effect for drawing point sprite particles. This computes the particle
        // animation entirely in the vertex shader: no per-particle CPU work required!
        private SASParticles sasParticles;
        public SASParticles SasParticles { get { return sasParticles; } }

        // An array of particles, treated as a circular queue.
        private ParticleVertex[] particles;
        public ParticleVertex[] Particles { get { return particles; } }

        // A vertex buffer holding our particles. This contains the same data as
        // the particles array, but copied across to where the GPU can access it.
        private DynamicVertexBuffer vertexBuffer;
        public DynamicVertexBuffer VertexBuffer { get { return vertexBuffer; } }

        // Vertex declaration describes the format of our ParticleVertex structure.
        private VertexDeclaration vertexDeclaration;
        public VertexDeclaration VertexDeclaration { get { return vertexDeclaration; } }

        // The particles array and vertex buffer are treated as a circular queue.
        // Initially, the entire contents of the array are free, because no particles
        // are in use. When a new particle is created, this is allocated from the
        // beginning of the array. If more than one particle is created, these will
        // always be stored in a consecutive block of array elements. Because all
        // particles last for the same amount of time, old particles will always be
        // removed in order from the start of this active particle region, so the
        // active and free regions will never be intermingled. Because the queue is
        // circular, there can be times when the active particle region wraps from the
        // end of the array back to the start. The queue uses modulo arithmetic to
        // handle these cases. For instance with a four entry queue we could have:
        //
        //      0
        //      1 - first active particle
        //      2 
        //      3 - first free particle
        //
        // In this case, particles 1 and 2 are active, while 3 and 4 are free.
        // Using modulo arithmetic we could also have:
        //
        //      0
        //      1 - first free particle
        //      2 
        //      3 - first active particle
        //
        // Here, 3 and 0 are active, while 1 and 2 are free.
        //
        // But wait! The full story is even more complex.
        //
        // When we create a new particle, we add them to our managed particles array.
        // We also need to copy this new data into the GPU vertex buffer, but we don't
        // want to do that straight away, because setting new data into a vertex buffer
        // can be an expensive operation. If we are going to be adding several particles
        // in a single frame, it is faster to initially just store them in our managed
        // array, and then later upload them all to the GPU in one single call. So our
        // queue also needs a region for storing new particles that have been added to
        // the managed array but not yet uploaded to the vertex buffer.
        //
        // Another issue occurs when old particles are retired. The CPU and GPU run
        // asynchronously, so the GPU will often still be busy drawing the previous
        // frame while the CPU is working on the next frame. This can cause a
        // synchronization problem if an old particle is retired, and then immediately
        // overwritten by a new one, because the CPU might try to change the contents
        // of the vertex buffer while the GPU is still busy drawing the old data from
        // it. Normally the graphics driver will take care of this by waiting until
        // the GPU has finished drawing inside the VertexBuffer.SetData call, but we
        // don't want to waste time waiting around every time we try to add a new
        // particle! To avoid this delay, we can specify the SetDataOptions.NoOverwrite
        // flag when we write to the vertex buffer. This basically means "I promise I
        // will never try to overwrite any data that the GPU might still be using, so
        // you can just go ahead and update the buffer straight away". To keep this
        // promise, we must avoid reusing vertices immediately after they are drawn.
        //
        // So in total, our queue contains four different regions:
        //
        // Vertices between firstActiveParticle and firstNewParticle are actively
        // being drawn, and exist in both the managed particles array and the GPU
        // vertex buffer.
        //
        // Vertices between firstNewParticle and firstFreeParticle are newly created,
        // and exist only in the managed particles array. These need to be uploaded
        // to the GPU at the start of the next draw call.
        //
        // Vertices between firstFreeParticle and firstRetiredParticle are free and
        // waiting to be allocated.
        //
        // Vertices between firstRetiredParticle and firstActiveParticle are no longer
        // being drawn, but were drawn recently enough that the GPU could still be
        // using them. These need to be kept around for a few more frames before they
        // can be reallocated.

        private int firstActiveParticle;
        private int firstNewParticle;
        private int firstFreeParticle;
        private int firstRetiredParticle;

        // Store the current time, in seconds.
        private float currentTime;

        // Count how many times Draw has been called. This is used to know
        // when it is safe to retire old particles back into the free list.
        private int drawCounter;

        // Shared random number generator.
        static Random random = new Random();
        public static Random Random { get { return random; } }

        // Render layer in which the particles will be drawn
        private RenderManager.Layers layer = RenderManager.Layers.Transparency;
        public RenderManager.Layers Layer { get { return layer; } set { layer = value; } }

        // Delegate executed when the particle system is about to be rendered
        RenderCommand.ExecuteCommandDelegate ExecuteRenderCommandDelegate = null;

        /// This event is raised every time a particle system is prepared before rendering
        public event RenderMaterialDelegate OnPrepareMaterial;

        #endregion

        #region Initialization

        /// <summary>
        /// Constructor.
        /// </summary>
        public ParticleSystem(string assetName, ResourceManager.ResourcePool pool)
        {
            this.resourcePool = pool;
            this.assetName = assetName;
            this.ExecuteRenderCommandDelegate = new RenderCommand.ExecuteCommandDelegate(OnExecuteRenderCommand);
        }

        protected override bool OnLoad(GraphicsDevice graphics, ContentManager contentManager)
        {
            sasParticles = Engine.Instance.ParticlesManager.Load(assetName, contentManager);

            if (sasParticles == null)
            {
                return false;
            }

            lock (Engine.ResourceLock)
            {
                particles = new ParticleVertex[sasParticles.Settings.MaxParticles];
                vertexDeclaration = new VertexDeclaration(graphics, ParticleVertex.VertexElements);

                int size = ParticleVertex.SizeInBytes * particles.Length;
                vertexBuffer = new DynamicVertexBuffer(graphics, size, BufferUsage.WriteOnly | BufferUsage.Points);
            }

            return true;
        }

        #endregion

        #region Update and Draw

        /// <summary>
        /// Updates the particle system.
        /// </summary>
        public void Update(float elapsedTime)
        {
            currentTime += elapsedTime;

            RetireActiveParticles();
            FreeRetiredParticles();

            // If we let our timer go on increasing for ever, it would eventually
            // run out of floating point precision, at which point the particles
            // would render incorrectly. An easy way to prevent this is to notice
            // that the time value doesn't matter when no particles are being drawn,
            // so we can reset it back to zero any time the active queue is empty.

            if (firstActiveParticle == firstFreeParticle)
                currentTime = 0;

            if (firstRetiredParticle == firstActiveParticle)
                drawCounter = 0;
        }


        /// <summary>
        /// Helper for checking when active particles have reached the end of
        /// their life. It moves old particles from the active area of the queue
        /// to the retired section.
        /// </summary>
        void RetireActiveParticles()
        {
            float particleDuration = sasParticles.Settings.Duration;

            while (firstActiveParticle != firstNewParticle)
            {
                // Is this particle old enough to retire?
                float particleAge = currentTime - particles[firstActiveParticle].Time;

                if (particleAge < particleDuration)
                    break;

                // Remember the time at which we retired this particle.
                particles[firstActiveParticle].Time = drawCounter;

                // Move the particle from the active to the retired queue.
                firstActiveParticle++;

                if (firstActiveParticle >= particles.Length)
                    firstActiveParticle = 0;
            }
        }


        /// <summary>
        /// Helper for checking when retired particles have been kept around long
        /// enough that we can be sure the GPU is no longer using them. It moves
        /// old particles from the retired area of the queue to the free section.
        /// </summary>
        void FreeRetiredParticles()
        {
            while (firstRetiredParticle != firstActiveParticle)
            {
                // Has this particle been unused long enough that
                // the GPU is sure to be finished with it?
                int age = drawCounter - (int)particles[firstRetiredParticle].Time;

                // The GPU is never supposed to get more than 2 frames behind the CPU.
                // We add 1 to that, just to be safe in case of buggy drivers that
                // might bend the rules and let the GPU get further behind.
                if (age < 3)
                    break;

                // Move the particle from the retired to the free queue.
                firstRetiredParticle++;

                if (firstRetiredParticle >= particles.Length)
                    firstRetiredParticle = 0;
            }
        }

        /// <summary>
        /// Prepares the material for rendering 
        /// </summary>
        public void OnExecuteRenderCommand(SASMaterial material)
        {
            material.Effect.Parameters["CurrentTime"].SetFloatValue(ref currentTime);
            if (OnPrepareMaterial != null) OnPrepareMaterial(this, material);
        }
        
        /// <summary>
        /// Draws the particle system.
        /// </summary>
        public void Render(ref Matrix worldMatrix, GraphicsDevice device)
        {
            // Restore the vertex buffer contents if the graphics device was lost.
            if (vertexBuffer.IsContentLost)
            {
                vertexBuffer.SetData(particles);
            }

            // If there are any particles waiting in the newly added queue,
            // we'd better upload them to the GPU ready for drawing.
            if (firstNewParticle != firstFreeParticle)
            {
                AddNewParticlesToVertexBuffer();
            }

            // If there are any active particles, draw them now!
            if (firstActiveParticle != firstFreeParticle)
            {
                RenderManager manager = Engine.Instance.RenderManager;

                if (firstActiveParticle < firstFreeParticle)
                {
                    // If the active particles are all in one consecutive range,
                    // we can draw them all in a single call.
                    RenderCommand command = Engine.Instance.RenderManager.BuildRenderCommand(
                        this,
                        firstActiveParticle,
                        firstFreeParticle - firstActiveParticle,
                        ref worldMatrix,
                        sasParticles);

                    command.OnExecuteCommand += ExecuteRenderCommandDelegate;
                    manager.AddCommand(command, layer);
                }
                else
                {
                    // If the active particle range wraps past the end of the queue
                    // back to the start, we must split them over two draw calls.
                    RenderCommand firstCommand = Engine.Instance.RenderManager.BuildRenderCommand(
                        this,
                        firstActiveParticle,
                        particles.Length - firstActiveParticle,
                        ref worldMatrix,
                        sasParticles);

                    firstCommand.OnExecuteCommand += ExecuteRenderCommandDelegate;
                    manager.AddCommand(firstCommand, layer);

                    if (firstFreeParticle > 0)
                    {
                        RenderCommand secondCommand = Engine.Instance.RenderManager.BuildRenderCommand(
                            this,
                            0,
                            firstFreeParticle,
                            ref worldMatrix,
                            sasParticles);

                        secondCommand.OnExecuteCommand += ExecuteRenderCommandDelegate;
                        manager.AddCommand(secondCommand, layer);
                    }
                }
            }

            drawCounter++;
        }


        /// <summary>
        /// Helper for uploading new particles from our managed
        /// array to the GPU vertex buffer.
        /// </summary>
        void AddNewParticlesToVertexBuffer()
        {
            int stride = ParticleVertex.SizeInBytes;

            if (firstNewParticle < firstFreeParticle)
            {
                // If the new particles are all in one consecutive range,
                // we can upload them all in a single call.
                vertexBuffer.SetData(firstNewParticle * stride, particles,
                                     firstNewParticle,
                                     firstFreeParticle - firstNewParticle,
                                     stride, SetDataOptions.NoOverwrite);
            }
            else
            {
                // If the new particle range wraps past the end of the queue
                // back to the start, we must split them over two upload calls.
                vertexBuffer.SetData(firstNewParticle * stride, particles,
                                     firstNewParticle,
                                     particles.Length - firstNewParticle,
                                     stride, SetDataOptions.NoOverwrite);

                if (firstFreeParticle > 0)
                {
                    vertexBuffer.SetData(0, particles,
                                         0, firstFreeParticle,
                                         stride, SetDataOptions.NoOverwrite);
                }
            }

            // Move the particles we just uploaded from the new to the active queue.
            firstNewParticle = firstFreeParticle;
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Adds a new particle to the system.
        /// </summary>
        public void AddParticle(Vector3 position, Vector3 velocity)
        {
            // Figure out where in the circular queue to allocate the new particle.
            int nextFreeParticle = firstFreeParticle + 1;

            if (nextFreeParticle >= particles.Length)
                nextFreeParticle = 0;

            // If there are no free particles, we just have to give up.
            if (nextFreeParticle == firstRetiredParticle)
                return;

            // Adjust the input velocity based on how much
            // this particle system wants to be affected by it.
            velocity *= sasParticles.Settings.EmitterVelocitySensitivity;

            // Choose four random control values. These will be used by the vertex
            // shader to give each particle a different size, rotation, and color.
            Color randomValues = new Color((byte)random.Next(255),
                                           (byte)random.Next(255),
                                           (byte)random.Next(255),
                                           (byte)random.Next(255));

            // Fill in the particle vertex structure.
            particles[firstFreeParticle].Position = position;
            particles[firstFreeParticle].Velocity = velocity;
            particles[firstFreeParticle].Random = randomValues;
            particles[firstFreeParticle].Time = currentTime;

            firstFreeParticle = nextFreeParticle;
        }


        #endregion
    }
}
